﻿using System;
using System.Collections.Generic;
using System.Drawing;
using System.Runtime.InteropServices;
using System.Text.RegularExpressions;
using System.Windows;
using System.Windows.Input;
using System.Windows.Interop;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Threading;
using Centrify.DirectAudit.Common.Terminal;
using Poderosa;
using Poderosa.Document;
using Poderosa.View;
using Point = System.Windows.Point;
using Size = System.Drawing.Size;

namespace Player
{
    public class UnixRenderEngine : RenderEngineBase
    {
        public UnixRenderEngine()
        {
            m_emulator.Options.BeepOnBellChar = false;
            ResizeTerminal(80, 24);
        }

        public bool HighlightText(Regex regex)
        {
            // Find out all the matched texts in the current terminal
            List<TextLocation> locations = new List<TextLocation>();
            GLine line = m_emulator.Document.FirstLine;
            while (line != null)
            {
                MatchCollection matches = regex.Matches(line.ToNormalString());
                foreach (Match match in matches)
                {
                    locations.Add(new TextLocation(line.ID, match.Index, match.Value));
                }
                if (line == m_emulator.Document.LastLine) break;
                line = line.NextLine;
            }

            if (locations.Count == 0) return false;

            // Highlight the matched texts
            m_emulator.HighlightLocations = locations;
            m_emulator.Invalidate();
            InvalidateVisual();

            // Clear the emulator highights so that when the session continues to play, all the highlight texts are gone
            m_emulator.HighlightLocations = null;
            return true;
        }

        private bool m_suspended;
        public override void SuspendScreenUpdate()
        {
            m_suspended = true;
            Image = m_interopBitmap.CloneCurrentValue();
        }

        public override void ResumeScreenUpdate()
        {
            m_suspended = false;
            Image = m_interopBitmap;

            InvalidateVisual();
        }

        public override void Process(FrameBase frame)
        {
            var f = frame as UnixFrame;
            if (f == null) return;

            switch ((UnixSession.Type)f.Type)
            {
                case UnixSession.Type.Unknown:
                    if (f.Data != null)
                    {
                        m_emulator.RestoreSnapshot(f.Data);
                    }
                    ResizeTerminal(m_emulator.Document.TerminalWidth, m_emulator.Document.TerminalHeight);
                    break;
                case UnixSession.Type.ResizeWindow:
                    ResizeTerminal(f.ScreenWidth, f.ScreenHeight);
                    break;
                case UnixSession.Type.StandardOut:
                    if (f.Data != null)
                    {
                        Console.WriteLine(System.Text.ASCIIEncoding.Default.GetString(f.Data));
                        m_emulator.Write(f.Data);
                    }
                    break;
            }

            if (!m_suspended)
            {
                InvalidateVisual();
            }
        }

        public void InvalidateVisual()
        {
            Rectangle rect = new Rectangle(0, 0, m_emulator.Size.Width, m_emulator.Size.Height);
            m_graphics.FillRectangle(System.Drawing.Brushes.Black, rect); // Clear the screen before rendering
            m_emulator.Render(m_graphics, rect);

            //Invalidate the InteropBitmap to trigger a redraw in WPF
            if (!m_interopBitmap.Dispatcher.CheckAccess())
            {
                m_interopBitmap.Dispatcher.BeginInvoke(DispatcherPriority.Normal, new Action(() => m_interopBitmap.Invalidate()));
            }
            else
            {
                m_interopBitmap.Invalidate();
            }
        }

        public override BitmapSource TakeSnapshot()
        {
            return m_interopBitmap.CloneCurrentValue();
        }

        private void ResizeTerminal(int width, int height)
        {
            m_emulator.ResizeTerminal(width, height);

            // We are going to create a shared memory section.
            // GDI is going to draw the graphic on it and WPF is going to display it
            // We ensure both of them are using the same color encoding
            //
            // (Actually, WPF is not really using the shared memory from GDI at all.  It's copying the data from shared memory to its own internal memory.
            //  However, InteropBitmap hides the implementation details from us.  It looks like we are sharing memory.)

            // Create a memory device context compatible with the current application screen
            IntPtr hdc = SafeNativeMethods.CreateCompatibleDC(IntPtr.Zero);

            // Create a memory section
            PixelFormat pixfmt = PixelFormats.Bgr32;  // Tell WPF we use RGB encooding with 32 bit per color code
            int bpp = pixfmt.BitsPerPixel;
            int stride = (m_emulator.Size.Width * (bpp / 8));
            uint byteCount = (uint)(stride * m_emulator.Size.Height);
            IntPtr m_WPFBitmapMemSection = SafeNativeMethods.CreateFileMapping(INVALID_HANDLE_VALUE, IntPtr.Zero, SafeNativeMethods.FileMappingPageProtection.PageReadWrite, 0, byteCount, null);

            SafeNativeMethods.BitmapInfo binfo = new SafeNativeMethods.BitmapInfo();
            binfo.bmiHeader = new SafeNativeMethods.BITMAPINFOHEADER();
            binfo.bmiColors = new SafeNativeMethods.RGBQUAD[256];
            binfo.bmiHeader.biBitCount = 32; // 32 bit color code
            binfo.bmiHeader.biSize = 40;
            binfo.bmiHeader.biPlanes = 1;
            binfo.bmiHeader.biCompression = BI_RGB;  // Tell GDI we use RGB encoding
            binfo.bmiHeader.biHeight = -m_emulator.Size.Height; // WPF scanlines go from top to bottom while GDI scanlines go from bottom to top.  Reverse it.
            binfo.bmiHeader.biWidth = m_emulator.Size.Width;

            // Create a DIB bitmap from memory section in the created DC
            IntPtr memory;
            IntPtr dib = SafeNativeMethods.CreateDIBSection(hdc, ref binfo, DIB_RGB_COLORS, out memory, m_WPFBitmapMemSection, 0);

            // Select the DIB bitmap
            SafeNativeMethods.SelectObject(hdc, dib);

            // Device context is setup properly.  Let's create a Graphic class for WinForm style UI
            m_graphics = Graphics.FromHdcInternal(hdc);

            // Use CreateBitmapSourceFromMemorySection to create InteropBitmap.  MSDN claims it's always InteropBitmap type.
            // Alternatively, we can copy the IntPtr memory to WriteableBitmap using Marshal.CopyMemory.  After some tests, InteropBitmap seems to have better performance sometimes.
            BitmapSource bmpSource = Imaging.CreateBitmapSourceFromMemorySection(m_WPFBitmapMemSection, m_emulator.Size.Width, m_emulator.Size.Height, pixfmt, stride, 0);
            m_interopBitmap = bmpSource as InteropBitmap;

            // Set our image's source to the InteropBitmap  
            Image = m_interopBitmap;
        }

        #region CopyFunction

        private void CopySelection()
        {
            TextSelection sel = m_emulator.TextSelection;

            if ((sel.State == SelectionState.Expansion || sel.State == SelectionState.Pivot) && !sel.IsEmpty)
            {
                //send the selection text directly to Clipboard
                string data = sel.GetSelectedText(TextFormatOption.Default);
                if (data != string.Empty)
                {
                    //if sometimes may be the Clipboard is locked by another process, It will raise a COMException:"OpenClipboard Failed (Exception from HRESULT: 0x800401D0 (CLIPBRD_E_CANT_OPEN))"
                    //so here try multiple times within a loop and sleep a few time in between
                    //refer: http://stackoverflow.com/questions/68666/clipbrd-e-cant-open-error-when-setting-the-clipboard-from-net
                    for (int i = 0; i < 10; i++)
                    {
                        try
                        {
                            Clipboard.SetDataObject(data, true);
                        }
                        catch (System.Runtime.InteropServices.COMException ex)
                        {
                            Log.Error(ex.Message);
                        }

                        System.Threading.Thread.Sleep(10);
                    }
                }
            }
        }

        private bool ClearSelection()
        {
            TextSelection sel = m_emulator.TextSelection;
            if (!sel.IsEmpty)
            {
                sel.Clear();
                //update visual
                this.m_emulator.Invalidate();
                return true;
            }

            return false;
        }

        /// <summary>
        /// Sets the selection begin point on mouse left button down
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="args"></param>
        public void OnMouseLeftButtonDown(object sender, MouseEventArgs args)
        {
            if (!m_emulator.Focused) m_emulator.Focus();
            CharacterDocument document = m_emulator.CharacterDocument;
            lock (document)
            {
                int col, row;
                var image = sender as System.Windows.Controls.Image;
                var pt = args.GetPosition(image);
                pt = TranslatePoint(image, pt);
                MousePosToTextPos(pt.X, pt.Y, out col, out row);
                int target_id = m_emulator.GetTopLine().ID + row;
                TextSelection sel = m_emulator.TextSelection;
                if (sel.State == SelectionState.Fixed) sel.Clear();
                if (target_id <= document.LastLineNumber)
                {
                    RangeType rt;

                    if (sel.StartX != pt.X || sel.StartY != pt.Y)
                        rt = RangeType.Char;
                    else
                        rt = sel.PivotType == RangeType.Char
                                 ? RangeType.Word
                                 : sel.PivotType == RangeType.Word ? RangeType.Line : RangeType.Char;

                    GLine tl = document.FindLine(target_id);
                    if (tl != null) sel.StartSelection(tl, col, rt, (int)pt.X, (int)pt.Y);
                }

                m_emulator.Invalidate();
            }
        }

        /// <summary>
        /// Drawing the selection area on mouse move
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="args"></param>
        public bool OnMouseMove(object sender, MouseEventArgs args)
        {
            if (args.LeftButton != MouseButtonState.Pressed) return false;

            var image = sender as System.Windows.Controls.Image;
            var pt = args.GetPosition(image);
            pt = TranslatePoint(image, pt);

            TextSelection sel = m_emulator.TextSelection;

            if (sel.State == SelectionState.Fixed || sel.State == SelectionState.Empty) return false;
            if (sel.StartX == pt.X && sel.StartY == pt.Y) return false;

            CharacterDocument document = m_emulator.CharacterDocument;
            lock (document)
            {
                int topline_id = m_emulator.GetTopLine().ID;
                Size pitch = m_emulator.GetRenderProfile().Pitch.ToSize();
                int row, col;
                MousePosToTextPos_AllowNegative(pt.X, pt.Y, out col, out row);
                int viewheight = (int)Math.Floor((double)m_emulator.ClientSize.Height / pitch.Width);
                int target_id = topline_id + row;

                GLine target_line = document.FindLineOrEdge(target_id);
                TextSelection.TextPoint point = sel.ConvertSelectionPosition(target_line, col);

                point.Line = RuntimeUtil.AdjustIntRange(point.Line, document.FirstLineNumber, document.LastLineNumber);

                point.Line = RuntimeUtil.AdjustIntRange(point.Line, topline_id, topline_id + viewheight - 1);

                RangeType rt = sel.PivotType;

                GLine tl = document.FindLine(point.Line);
                sel.ExpandTo(tl, point.Column, rt);
            }

            m_emulator.Invalidate();
            return true;
        }

        /// <summary>
        /// If 'CTRL+C' do the copy no clear selection
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        public void OnKeyDown(object sender, KeyEventArgs e)
        {
            //CTRL+C
            if (Keyboard.Modifiers == ModifierKeys.Control && e.Key == Key.C)
            {
                CopySelection();
            }
        }

        /// <summary>
        /// Do the copy & clear selection on mouse right button down.
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="args"></param>
        /// <returns></returns>
        public bool OnMouseRightButtonDown(object sender, MouseEventArgs args)
        {
            CopySelection();
            var needInvalidateUI = ClearSelection();
            return needInvalidateUI;
        }

        private Point TranslatePoint(System.Windows.Controls.Image image, Point pt)
        {
            var zoomWPercent = image.ActualWidth / m_emulator.Size.Width;
            var zoomHPercent = image.ActualHeight / m_emulator.Size.Height;
            pt = new Point(pt.X / zoomWPercent, pt.Y / zoomHPercent);
            return pt;
        }

        private void MousePosToTextPos(double mouseX, double mouseY, out int textX, out int textY)
        {
            Size pitch = m_emulator.GetRenderProfile().Pitch.ToSize();
            textX = RuntimeUtil.AdjustIntRange((int)Math.Floor((mouseX - 2) / pitch.Width), 0, Int32.MaxValue);
            textY = RuntimeUtil.AdjustIntRange((int)Math.Floor((mouseY - 2) / pitch.Height), 0, Int32.MaxValue);
        }

        private void MousePosToTextPos_AllowNegative(double mouseX, double mouseY, out int textX, out int textY)
        {
            Size pitch = m_emulator.GetRenderProfile().Pitch.ToSize();
            textX = (int)Math.Floor((mouseX - 2) / pitch.Width);
            textY = (int)Math.Floor((mouseY - 2) / pitch.Height);
        }

        #endregion

        public static IntPtr INVALID_HANDLE_VALUE = new IntPtr(-1);

        private const uint DIB_RGB_COLORS = 0;
        private const uint DIB_PAL_COLORS = 1;

        // Bitmap compression methods
        private const uint BI_RGB = 0;
        private const uint BI_RLE8 = 1;
        private const uint BI_RLE4 = 2;
        private const uint BI_BITFIELDS = 3;
        private const uint BI_JPEG = 4;
        private const uint BI_PNG = 5;

        private InteropBitmap m_interopBitmap;
        private TerminalEmulator m_emulator = new TerminalEmulator();
        private Graphics m_graphics;

        internal static class SafeNativeMethods
        {
            #region Enums and Structures
            [StructLayout(LayoutKind.Sequential)]
            public struct BITMAPINFOHEADER
            {
                public uint biSize;
                public int biWidth;
                public int biHeight;
                public ushort biPlanes;
                public ushort biBitCount;
                public uint biCompression;
                public uint biSizeImage;
                public int biXPelsPerMeter;
                public int biYPelsPerMeter;
                public uint biClrUsed;
                public uint biClrImportant;
            }

            [StructLayout(LayoutKind.Sequential)]
            public struct RGBQUAD
            {
                public byte rgbBlue;
                public byte rgbGreen;
                public byte rgbRed;
                public byte rgbReserved;
            }

            [StructLayout(LayoutKind.Sequential)]
            public struct BitmapInfo
            {
                public BITMAPINFOHEADER bmiHeader;
                [MarshalAs(UnmanagedType.ByValArray, SizeConst = 256)]
                public RGBQUAD[] bmiColors;
            }

            [Flags]
            public enum FileMappingPageProtection : uint
            {
                PageReadonly = 0x02,
                PageReadWrite = 0x04,
                PageWriteCopy = 0x08,
                PageExecuteRead = 0x20,
                PageExecuteReadWrite = 0x40,
                SectionCommit = 0x8000000,
                SectionImage = 0x1000000,
                SectionNoCache = 0x10000000,
                SectionReserve = 0x4000000,
            }
            #endregion

            [DllImport("gdi32.dll", SetLastError = true)]
            public static extern IntPtr CreateCompatibleDC(IntPtr hdc);

            [DllImport("gdi32.dll", SetLastError = true)]
            public static extern IntPtr CreateDIBSection(IntPtr hdc, [In] ref BitmapInfo pbmi, uint iUsage,
                                                            out IntPtr ppvBits, IntPtr hSection, uint dwOffset);

            [DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Auto)]
            public static extern IntPtr CreateFileMapping(IntPtr hFile,
               IntPtr lpFileMappingAttributes,
               FileMappingPageProtection flProtect,
               uint dwMaximumSizeHigh,
               uint dwMaximumSizeLow,
               [MarshalAs(UnmanagedType.LPTStr)] string lpName);

            [DllImport("gdi32.dll", ExactSpelling = true, PreserveSig = true, SetLastError = true)]
            public static extern IntPtr SelectObject(IntPtr hdc, IntPtr hgdiobj);
        }
    }
}
